/* pmt.c 支持两个最多2^16 行(65536行)数据间的组合
 * 限制：
 *     输入文件，每行所占字节数 <= 4096。常见的windows和linux文件系统会限制文件名长度在255以内，这很容易达到
 *     请确保你的设备有足够的内存容纳两个待组合的文件，即你的空闲内存大于两个待组合的文件大小之和+2静态字符串指针数组(1MB)+程序本身大小。
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <ctype.h>
#include <string.h>
#include <getopt.h>
#include <stdbool.h>

#define STRARR_LEN (65536)  // 数组内容达到2^16个，8字节指针则为2^19字节。单数组占用0.5MB，两个数组占用1MB

enum _组合模式 {all=0,全组合=0, tl=1,lt=1,左上三角=1, tr=2,rt=2,右上三角=2, bl=3,lb=3,左下三角=3, br=4,rb=4,右下三角=4, u=5,t=5,上三角=5, l=6,b=6,下三角=6};

void 打印说明(FILE *stream, int exit_code);
void 打印帮助();
void 打印版本();

int 获取组合模式(char *组合模式串);
int 读取文件到字符串指针数组(char *strarr[], int n, FILE *fp);
char * 获取一行(FILE * fp);

int 打印全组合矩阵(FILE * 输出文件, char * 行数组1[], int 行数1, char * 行数组2[], int 行数2, char 分隔串[]); //全组合矩阵

int 打印上三角矩阵(FILE * 输出文件, char * 行数组1[], int 行数1, char * 行数组2[], int 行数2, char 分隔串[]); //上三角矩阵
int 打印下三角矩阵(FILE * 输出文件, char * 行数组1[], int 行数1, char * 行数组2[], int 行数2, char 分隔串[]); //下三角矩阵
// 四角小三角
int 打印左上三角矩阵(FILE * 输出文件, char * 行数组1[], int 行数1, char * 行数组2[], int 行数2, char 分隔串[]);
int 打印右上三角矩阵(FILE * 输出文件, char * 行数组1[], int 行数1, char * 行数组2[], int 行数2, char 分隔串[]);
int 打印左下三角矩阵(FILE * 输出文件, char * 行数组1[], int 行数1, char * 行数组2[], int 行数2, char 分隔串[]);
int 打印右下三角矩阵(FILE * 输出文件, char * 行数组1[], int 行数1, char * 行数组2[], int 行数2, char 分隔串[]);

static struct option long_options[] = {
    {"input",     required_argument, NULL, 'i'},     // 输入文件：一个或两个
    {"model",     required_argument, NULL, 'm'},     // 子矩阵选择：选择需要输出的子矩阵部分
    {"delimiter", required_argument, NULL, 'd'},     // 分隔符：默认为制表符、可改为逗号或空格
    {"fenge",     required_argument, NULL, 'f'},     // 分隔(fenge)符：默认为制表符、可改为逗号或空格
    {"output",    required_argument, NULL, 'o'},     // 输出文件：没有的话输出到标准输出 stdout
    {"help",      no_argument,       NULL, 'h'},     // 帮助
    {"bangzhu",   no_argument,       NULL, 'b'},     // 帮助
    {"version",   no_argument,       NULL, 'v'},     // 版本信息
    {"banben",    no_argument,       NULL, 'v'},     // 版本信息
    {0, 0, 0, 0}                                     // 防止内存溢出，有一个空项，指示结束
};

FILE *输入文件1, *输入文件2, *输出文件;  // 两个输入文件，一个输出文件

char * 分隔串 = "\t";    // 分隔字符串，默认使用单个制表符

int (*组合打印函数)(FILE * 输出文件, char * 行数组1[], int 行数1, char * 行数组2[], int 行数2, char 分隔串[]) = 打印全组合矩阵;

int main(int argc, char *argv[]) {
    输入文件1 = stdin;
    输入文件2 = stdin;
    输出文件 = stdout;
    char * 文件名;       // 临时文件名
    int 输入文件数 = 0;  // 输入文件计数，0时完全使用管道，1时为单组合方阵，2时为双组合矩阵

    enum _组合模式 组合模式=all;   // 默认输出整个矩阵所有组合

    /* 处理参数列表，初始化：输入文件数计数、输入文件1 输入文件2、输出文件、
     * 定界符、组合模式
     */
    int opt_char = 0;
    while((opt_char=getopt_long_only(argc, argv, "i:m:d:f:o:hv", long_options, NULL))!=-1) {
        switch (opt_char) {
        case 'm':
            // printf("M model: %s\n", optarg);
            组合模式 = 获取组合模式(optarg);
            if (组合模式 == -1) {
                fprintf(stderr, "不支持的组合模式，请查看说明！\n");
                exit(EXIT_FAILURE);
            }
            // fprintf(stderr, "M model_code: %i\n", model);
            break;
        case 'i': //指定输入文件，1个或2个文件，支持使用横杠 - 表明使用来自管道的数据作为其中一个输入文件
            文件名 = optarg;
            // fprintf(stderr, "file 1: %s\n", 文件名);
            // 打开文件1
            if (文件名[0]=='-' && 文件名[1]=='\0') { // 文件1是管道引用
                输入文件1 = stdin;
                // fprintf(stderr, "file 1: stdin\n");
            }else if ((输入文件1=fopen(文件名, "r+"))==NULL) { // 不是管道引用，而是文件名
                fprintf(stderr, "打开输入文件1 %s 错误!\n", 文件名);
                exit(EXIT_FAILURE);
            } else {
                输入文件数 = 1;
            }
            // printf("optind=%i\t argv[optind]=%s\n", optind, argv[optind]);
            // 检查文件2是否存在
            if (optind<argc && !(argv[optind][0]=='-' && argv[optind][1]!='\0')) { // 存在第二文件: 或者下一个为新的参数键
                文件名 = argv[optind];
                // fprintf(stderr, "file 2: %s\n", 文件名);
                if (argv[optind][0]=='-' && argv[optind][1]=='\0') {    // 如果为管道符 '-\0'
                    输入文件2 = stdin;
                    // fprintf(stderr, "file 2: stdin\n");
                }else if ((输入文件2=fopen(文件名, "r+"))==NULL) { // 打不是管道引用，而是文件名
                    fprintf(stderr, "打开输入文件2 %s 错误!\n", 文件名);
                    exit(EXIT_FAILURE);
                }
                输入文件数 = 2;
            }
            // fprintf(stderr, "输入文件数: %i\n", 输入文件数);
            break;
        case 'd': case 'f':  // 分隔串
            // fprintf(stderr, "d 分隔串: %s\n", optarg);
            分隔串 = optarg;
            break;
        case 'o':           // 打开 输出文件
            文件名 = optarg;
            if ((输出文件=fopen(文件名, "w+"))==NULL) {
                fprintf(stderr, "打开文件 %s 错误!\n", 文件名);
                exit(EXIT_FAILURE);
            }
            // fprintf(stderr, "o: optind=%i\t optarg=%s\n", optind, optarg);
            break;
        case 'h': case 'b':
            打印帮助();
            exit(EXIT_SUCCESS);
            break;
        case 'v':
            打印版本();
            exit(EXIT_SUCCESS);
            break;
        case '?':
            fprintf(stderr, "参数无值 %s\n请检查参数值\n", argv[optind-1]);
            break;
        default:
            fprintf(stderr, "不明参数: %c\n", opt_char);
            break;
        }
    }

    // 两文件的字符串数组
    char *字符串指针数组1[STRARR_LEN], *字符串指针数组2[STRARR_LEN];    // 来自两个文件的字符串
    long  行数1=0, 行数2=0;  //两文件有效行数

    // 根据组合模式，设定 组合打印函数
    switch (组合模式) {
        case 全组合:   // 整个矩阵
            组合打印函数 = 打印全组合矩阵;
            break;
        // 大三角矩阵
        case 上三角:     // 上三角
            组合打印函数 = 打印上三角矩阵;
            break;
        case 下三角:     // 下三角
            组合打印函数 = 打印下三角矩阵;
            break;
        // 小三角矩阵
        case 左上三角:    // 左上三角
            组合打印函数 = 打印左上三角矩阵;
            break;
        case 右上三角:    // 右上三角
            组合打印函数 = 打印右上三角矩阵;
            break;
        case 左下三角:    // 左下三角
            组合打印函数 = 打印左下三角矩阵;
            break;
        case 右下三角:    // 右下三角
            组合打印函数 = 打印右下三角矩阵;
            break;
        // 不支持的非法组合
        default:
            fprintf(stderr, "不支持该组合模式！\n");
            exit(EXIT_FAILURE);
    }
    // 根据不同的输入文件数，使用不同的参数调用 组合打印函数
    if (输入文件数==0 || 输入文件数==1) {
        // 1个输入文件
        行数1 = 读取文件到字符串指针数组(字符串指针数组1, STRARR_LEN, 输入文件1); // 读入整个文件到 字符串指针数组中
        组合打印函数(输出文件, 字符串指针数组1, 行数1, 字符串指针数组1, 行数1, 分隔串);
    } else if (输入文件数 == 2) {
        // 2个输入文件
        行数1 = 读取文件到字符串指针数组(字符串指针数组1, STRARR_LEN, 输入文件1); // 读入整个文件到 字符串指针数组中
        行数2 = 读取文件到字符串指针数组(字符串指针数组2, STRARR_LEN, 输入文件2);
        组合打印函数(输出文件, 字符串指针数组1, 行数1, 字符串指针数组2, 行数2, 分隔串);
    }

    // 关闭文件，逆序关闭, 注意stdin和stdout 不能关闭
    // fclose(输出文件);
    // fclose(输入文件2);
    // fclose(输入文件1);
    return 0;
}

void 打印说明(FILE *stream, int exit_code) {
    fprintf(stream, "用法：%s options \n", "arg");
    fprintf(stream, " -h --help 帮助\n"
                    " -o --output 输出文件名\n"
                    " -v --version 版本信息\n" );
    exit(exit_code);
}

// 根据模式字符串返回所需矩阵编码值，如果非法则返回-1
int 获取组合模式(char *组合模式串) {
    char 模式串[8];
    int 组合模式 = 全组合;
    // 双字映射表
    int 模式串_模式映射表[2][2] = {
    //     左       右
    /*上*/{左上三角, 右上三角},
    /*下*/{左下三角, 右下三角},
    };
    // 判断模式串长度合法性
    if (strlen(组合模式串) > 2) {
        return -1;
    }
    // 读入模式字符，并转换为小写
    模式串[0] = tolower(组合模式串[0]);
    模式串[1] = tolower(组合模式串[1]);
    // 开始映射
    if (模式串[1]=='\0') { // 如果为单字符：上三角或者下三角
        switch (模式串[0]) {
        case 'a': case 'q': // all 或者 全(quan)三角
            组合模式 = 全组合;
            break;
        case 'u': case 't': case 's': // 上(shang)三角
            组合模式 = 上三角;
            break;
        case 'l': case 'b': case 'x': // 下(xia)三角
            组合模式 = 下三角;
            break;
        default:
            组合模式 = -1;
            break;
        }
    } else { // 如果为双字符
        int i, j;
        // 上下矩阵判定 top bottom 上(shang)下(xia)
        if ((模式串[0]=='t'||模式串[1]=='t') || (模式串[0]=='s'||模式串[1]=='s')) {
            i = 0;
        }else if ((模式串[0]=='b'||模式串[1]=='b') || (模式串[0]=='x'||模式串[1]=='x')) {
            i = 1;
        }
        // 左右矩阵判定 left right 左(zuo)右(you)
        if ((模式串[0]=='l'||模式串[1]=='l') || (模式串[0]=='z'||模式串[1]=='z')){
            j = 0;
        }else if ((模式串[0]=='r'||模式串[1]=='r') || (模式串[0]=='y'||模式串[1]=='y')) {
            j = 1;
        }
        // 映射类型编码
        组合模式 = 模式串_模式映射表[i][j];
    }
    return 组合模式;
}

// 从文件中读取非空行,将每行非空字符串放入字符串数组
// 返回读取的有效行数(有效记录数),即排除空行外的行数
int 读取文件到字符串指针数组(char *strarr[], int n, FILE *fp) {
    char * str;
    int i;
    for (i=0; i<n && !feof(fp); ) {
        if ((str=获取一行(fp)) != NULL) { // 文件结束检查
            strarr[i] = str;
            i++;
        }
    }
    if ((str=获取一行(fp)) != NULL) {
        fprintf(stderr, "溢出错误: 文件行数超出数组容量上限\n最大行数容量: %i\n", n);
        exit(EXIT_FAILURE);
    }
    return i;
}

// 获取一行: 给定一文件指针 fp, 从该文件当前位置读取一行放入动态内存中，返回指向该动态内存块的字符串指针
// 会忽略空行 \n
char * 获取一行(FILE * fp) {
    char * tmpstr;
    static char 缓冲串[4096]; //支持最大 4095 字节一行
    char ch;
    // 读入一行
    int i = 0;
    while ((ch=getc(fp)) != EOF && ch != '\n') {
        缓冲串[i] = ch;
        i++;
    }
    缓冲串[i] = '\0'; //抹掉字符串末尾换行符 \n
    // 文件末尾空行
    if (缓冲串[0] == '\0') {
        return NULL;
    }
    // 申请内存
    tmpstr = (char *)malloc(strlen(缓冲串)+1);
    strncpy(tmpstr, 缓冲串, strlen(缓冲串)+1);
    
    return tmpstr;
}

// 输出整个矩阵组合 all
int 打印全组合矩阵(FILE * 输出文件, char * 行数组1[], int 行数1, char * 行数组2[], int 行数2, char 分隔串[]) {
    for (int i=0; i<行数1; i++) {
        for (int j=0; j<行数2; j++) {
            fprintf(输出文件, "%s%s%s\n", 行数组1[i], 分隔串, 行数组2[j]);
        }
    }
    return 0;
}
// 输出上三角矩阵 Upper Triangular
int 打印上三角矩阵(FILE * 输出文件, char * 行数组1[], int 行数1, char * 行数组2[], int 行数2, char 分隔串[]) {
    for (int i=0; i<行数1; i++) {
        for (int j=i; j<行数2; j++) {
            fprintf(输出文件, "%s%s%s\n", 行数组1[i], 分隔串, 行数组2[j]);
        }
    }
    return 0;
}
// 输出下三角矩阵 Lower Triangular
int 打印下三角矩阵(FILE * 输出文件, char * 行数组1[], int 行数1, char * 行数组2[], int 行数2, char 分隔串[]) {
    for (int i=0; i<行数1; i++) {
        for (int j=0; j<=i; j++) {
            fprintf(输出文件, "%s%s%s\n", 行数组1[i], 分隔串, 行数组2[j]);
        }
    }
    return 0;
}
// 输出左上三角矩阵 left top Triangular
int 打印左上三角矩阵(FILE * 输出文件, char * 行数组1[], int 行数1, char * 行数组2[], int 行数2, char 分隔串[]) {
    for (int i=0; i<行数1; i++) {
        for (int j=0; j<行数2-i-1; j++) {
            fprintf(输出文件, "%s%s%s\n", 行数组1[i], 分隔串, 行数组2[j]);
        }
    }
    return 0;
}
// 输出右上三角矩阵 right top Triangular
int 打印右上三角矩阵(FILE * 输出文件, char * 行数组1[], int 行数1, char * 行数组2[], int 行数2, char 分隔串[]) {
    for (int i=0; i<行数1; i++) {
        for (int j=i+1; j<行数2; j++) {
            fprintf(输出文件, "%s%s%s\n", 行数组1[i], 分隔串, 行数组2[j]);
        }
    }
    return 0;
}
// 输出左下三角矩阵 left top Triangular
int 打印左下三角矩阵(FILE * 输出文件, char * 行数组1[], int 行数1, char * 行数组2[], int 行数2, char 分隔串[]) {
    for (int i=1; i<行数1; i++) {
        for (int j=0; j<i; j++) {
            fprintf(输出文件, "%s%s%s\n", 行数组1[i], 分隔串, 行数组2[j]);
        }
    }
    return 0;
}
// 输出右下三角矩阵 right top Triangular
int 打印右下三角矩阵(FILE * 输出文件, char * 行数组1[], int 行数1, char * 行数组2[], int 行数2, char 分隔串[]) {
    for (int i=1; i<行数1; i++) {
        for (int j=行数2-i; j<行数2; j++) {
            fprintf(输出文件, "%s%s%s\n", 行数组1[i], 分隔串, 行数组2[j]);
        }
    }
    return 0;
}

void 打印帮助() {
    printf("用法： pmt [参数] \n");
    printf("参数:\n");
    printf( "    -i <输入文件1> [输入文件2]  指定输入文件，可以指定1个或2个输入文件，可以使用减号代指来自管道的标准输入\n"
            "                                不指定则默认使用来自管道的标准输入\n"
            "    -d 分隔串                   输出所用的分隔字符串，默认采用单个制表符分隔各列\n"
            "    -m 模式                     指定组合矩阵的输出部分，默认输出整个全组合矩阵中的所有组合\n"
            "                                s  矩阵上(shang)三角\n"
            "                                x  矩阵下(xia)三角\n"
            "                                ys 矩阵右(you)上(shang)三角\n"
            "                                zx 矩阵左(zuo)下(xia)三角\n"
            "    -o <输出文件>               输出到指定文件\n"
            "\n示例：使用来自文件 123.txt 和 abc.txt 的两个文件，输出组合矩阵的右(you)上(shang)三角，采用\"##\"作为输出分隔符，输出到文件out.txt中\n"
            "    pmt -m ys -d \"##\" -i 123.txt abc.txt -o out.txt\n");
}

void 打印版本() {
    printf("pmt v1.1\n");
}